/*
 * Copyright (c) 2011-2021, baomidou (jobob@qq.com).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cc.siyecao.mapper.provider.handlers;

import cc.siyecao.mapper.common.exception.AssertException;
import cc.siyecao.mapper.common.util.Assert;
import cc.siyecao.mapper.common.util.JsonUtil;
import cc.siyecao.mapper.common.util.StringUtils;
import cc.siyecao.mapper.provider.annotation.FieldFill;
import cc.siyecao.mapper.provider.annotation.Key;
import cc.siyecao.mapper.provider.annotation.Version;
import cc.siyecao.mapper.provider.entity.EntityColumn;
import cc.siyecao.mapper.provider.entity.EntityFactory;
import cc.siyecao.mapper.provider.entity.EntityField;
import cc.siyecao.mapper.provider.entity.EntityTable;
import cc.siyecao.mapper.provider.enums.FillStrategy;
import cc.siyecao.mapper.provider.enums.KeyType;
import cc.siyecao.mapper.provider.incrementer.IdentifierGenerator;
import cc.siyecao.mapper.provider.util.ServiceLoaderUtil;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.*;

import javax.persistence.Id;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static cc.siyecao.mapper.common.core.Code.FIND_MULT_IMPL;

/**
 * 自定义 ParameterHandler 重装构造函数，填充插入方法主键 ID
 *
 * @author lyt 2021/12/5
 */
public class MybatisParameterHandler implements ParameterHandler {

    private final TypeHandlerRegistry typeHandlerRegistry;
    private final MappedStatement mappedStatement;
    private final Object parameterObject;
    private final BoundSql newBoundSql;
    private final Configuration configuration;
    private final SqlCommandType sqlCommandType;

    public MybatisParameterHandler(MappedStatement mappedStatement, Object parameter, BoundSql boundSql) {
        this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
        this.mappedStatement = mappedStatement;
        this.newBoundSql = boundSql;
        this.configuration = mappedStatement.getConfiguration();
        this.sqlCommandType = mappedStatement.getSqlCommandType();
        this.parameterObject = processParameter( parameter );

    }

    public Object processParameter(Object parameter) {
        /* 只处理插入或更新操作 */
        if (parameter != null && (SqlCommandType.INSERT == this.sqlCommandType || SqlCommandType.UPDATE == this.sqlCommandType)) {
            //检查 parameterObject
            if (SimpleTypeRegistry.isSimpleType( parameter.getClass() )) {
                return parameter;
            }
            process( parameter );
        }
        return parameter;
    }

    @Override
    public Object getParameterObject() {
        return this.parameterObject;
    }

    private void process(Object parameter) {
        if (parameter != null) {
            EntityTable entityTable = null;
            Object entity = parameter;
            if (parameter instanceof Map) {
                Map<?, ?> map = (Map<?, ?>) parameter;
                if (map.get( "entity" ) != null) {
                    entity = map.get( "entity" );
                    entityTable = EntityFactory.create( entity.getClass() );
                }
//                if (map.get( "wrapper" ) != null) {
//
//                }
            } else if (parameter instanceof List) {
                List parameters = (List) parameter;
                parameters.forEach( this::process );
            } else {
                entityTable = EntityFactory.create( entity.getClass() );
            }
            if (entityTable != null) {
                //到这里就应该转换到实体参数对象了,因为填充和ID处理都是针对实体对象处理的,不用传递原参数对象下去.
                MetaObject metaObject = this.configuration.newMetaObject( entity );
                List<EntityColumn> entityColumns = entityTable.columns();
                for (Iterator<EntityColumn> iterator = entityColumns.iterator(); iterator.hasNext(); ) {
                    EntityColumn entityColumn = iterator.next();
                    EntityField entityField = entityColumn.field();
                    if (SqlCommandType.INSERT == this.sqlCommandType) {
                        if (entityField.isAnnotationPresent( Id.class ) && entityField.isAnnotationPresent( Key.class )) {
                            populateKeys( entityColumn, entityField, metaObject );
                        }
                        if (entityField.isAnnotationPresent( Version.class )) {
                            populateVersion( entityField, metaObject );
                        }
                    }
                    if (entityField.isAnnotationPresent( FieldFill.class )) {
                        FieldFill fieldFill = entityField.getAnnotation( FieldFill.class );
                        String fieldName = entityField.getName();
                        if (SqlCommandType.INSERT == this.sqlCommandType) {
                            if (fieldFill.fill() == FillStrategy.INSERT || fieldFill.fill() == FillStrategy.INSERT_UPDATE) {
                                if (fieldFill.force()) {
                                    insertFill( metaObject, fieldName );
                                } else if (StringUtils.checkValNull( getFieldValByName( fieldName, metaObject ) )) {
                                    insertFill( metaObject, fieldName );
                                }
                            }
                        } else if (SqlCommandType.UPDATE == this.sqlCommandType) {
                            if (fieldFill.fill() == FillStrategy.UPDATE || fieldFill.fill() == FillStrategy.INSERT_UPDATE) {
                                if (fieldFill.force()) {
                                    updateFill( metaObject, fieldName );
                                } else if (StringUtils.checkValNull( getFieldValByName( fieldName, metaObject ) )) {
                                    updateFill( metaObject, fieldName );
                                }
                            }
                        }
                    }
                }
                System.out.println( "++++++++++++++entity+++++++++++++++++++" + JsonUtil.obj2String( entity ) );
                System.out.println( "++++++++++++++parameter+++++++++++++++++++" + JsonUtil.obj2String( parameter ) );
            }
        }
    }

    protected void populateVersion(EntityField entityField, MetaObject metaObject) {
        String keyProperty = entityField.getName();
        metaObject.setValue( keyProperty, 1 );
    }

    protected void populateKeys(EntityColumn entityColumn, EntityField entityField, MetaObject metaObject) {
        String keyProperty = entityField.getName();
        Object idValue = metaObject.getValue( keyProperty );
        if (StringUtils.checkValNull( idValue )) {
            Key key = entityField.getAnnotation( Key.class );
            Class idClass = key.generator();
            String seqName = key.seqName();
            KeyType keyType = key.type();
            if (keyType == KeyType.GENERATOR && idClass != null && idClass != Class.class) {
                final IdentifierGenerator identifierGenerator;
                try {
                    identifierGenerator = (IdentifierGenerator) idClass.newInstance();
                } catch (Exception e) {
                    throw new AssertException( "IdentifierGenerator type '" + idClass.getName() + "' not supported" );
                }
                Object objId = identifierGenerator.nextId();
                Class<?> fieldType = entityField.getType();
                if (Number.class.isAssignableFrom( fieldType )) {
                    try {
                        Number id = (Number) objId;
                        if (fieldType == id.getClass()) {
                            metaObject.setValue( keyProperty, id );
                        } else if (Integer.class == fieldType) {
                            metaObject.setValue( keyProperty, id.intValue() );
                        } else if (Long.class == fieldType) {
                            metaObject.setValue( keyProperty, id.longValue() );
                        } else if (BigDecimal.class.isAssignableFrom( fieldType )) {
                            metaObject.setValue( keyProperty, new BigDecimal( id.longValue() ) );
                        } else if (BigInteger.class.isAssignableFrom( fieldType )) {
                            metaObject.setValue( keyProperty, new BigInteger( id.toString() ) );
                        } else {
                            throw new AssertException( "Key type '" + keyType + "' not supported" );
                        }
                    } catch (ClassCastException e) {
                        throw new AssertException( idClass.getName() + ".nextId() 返回类型与主键类型不匹配" );
                    }
                } else {
                    metaObject.setValue( keyProperty, identifierGenerator.nextId().toString() );
                }
            } else if (keyType == KeyType.SEQUENCE && StringUtils.isNotEmpty( seqName )) {
                Connection connection = null;
                PreparedStatement stmt = null;
                ResultSet rs = null;
                try {
                    connection = this.configuration.getEnvironment().getDataSource().getConnection();
                    String sql = "SELECT " + seqName + ".NEXTVAL AS ID FROM DUAL";
                    stmt = connection.prepareStatement( sql );
                    rs = stmt.executeQuery();
                    rs.next();
                    Long id = rs.getLong( 1 );
                    metaObject.setValue( keyProperty, id );
                } catch (SQLException e) {
                    e.printStackTrace();
                } finally {
                    if (connection != null) {
                        try {
                            connection.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                    if (stmt != null) {
                        try {
                            stmt.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                    if (rs != null) {
                        try {
                            rs.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                }
            } else {
                Connection connection = null;
                PreparedStatement stmt = null;
                ResultSet rs = null;
                try {
                    connection = this.configuration.getEnvironment().getDataSource().getConnection();
                    stmt = connection.prepareStatement( "SELECT max(" + entityColumn.column() + ") FROM " + entityColumn.entityTable().table() );
                    rs = stmt.executeQuery();
                    rs.next();
                    Long id = rs.getLong( 1 );
                    System.out.println( "id = " + id );
                    metaObject.setValue( keyProperty, id + 1 );
                } catch (SQLException e) {
                    e.printStackTrace();
                } finally {
                    if (connection != null) {
                        try {
                            connection.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                    if (stmt != null) {
                        try {
                            stmt.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                    if (rs != null) {
                        try {
                            rs.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }

    protected void insertFill(MetaObject metaObject, String fieldName) {
        List<MetaObjectHandler> metaObjectHandlers = ServiceLoaderUtil.getInstances( MetaObjectHandler.class );
        if (metaObjectHandlers != null && metaObjectHandlers.size() > 0) {
            Assert.isTrue( metaObjectHandlers.size() == 1, FIND_MULT_IMPL );
            MetaObjectHandler metaObjectHandler = metaObjectHandlers.get( 0 );
            Map<String, Object> fillMap = metaObjectHandler.insertFill();
            Object fieldVal = fillMap.get( fieldName );
            if (Objects.nonNull( fieldVal ) && metaObject.hasSetter( fieldName )) {
                metaObject.setValue( fieldName, fieldVal );
            }
        }
    }

    protected void updateFill(MetaObject metaObject, String fieldName) {
        List<MetaObjectHandler> metaObjectHandlers = ServiceLoaderUtil.getInstances( MetaObjectHandler.class );
        if (metaObjectHandlers != null && metaObjectHandlers.size() > 0) {
            Assert.isTrue( metaObjectHandlers.size() == 1, FIND_MULT_IMPL );
            MetaObjectHandler metaObjectHandler = metaObjectHandlers.get( 0 );
            Map<String, Object> fillMap = metaObjectHandler.updateFill();
            Object fieldVal = fillMap.get( fieldName );
            if (Objects.nonNull( fieldVal ) && metaObject.hasSetter( fieldName )) {
                metaObject.setValue( fieldName, fieldVal );
            }
        }
    }

    protected Object getFieldValByName(String fieldName, MetaObject metaObject) {
        return metaObject.hasGetter( fieldName ) ? metaObject.getValue( fieldName ) : null;
    }

    @Override
    @SuppressWarnings("unchecked")
    public void setParameters(PreparedStatement ps) {
        ErrorContext.instance().activity( "setting parameters" ).object( this.mappedStatement.getParameterMap().getId() );
        List<ParameterMapping> parameterMappings = this.newBoundSql.getParameterMappings();
        if (parameterMappings != null) {
            for (int i = 0; i < parameterMappings.size(); i++) {
                ParameterMapping parameterMapping = parameterMappings.get( i );
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    Object value;
                    String propertyName = parameterMapping.getProperty();
                    System.out.println( "++++++++++++++++++++++propertyName++++++++++++++++++++++" + propertyName );
                    if (this.newBoundSql.hasAdditionalParameter( propertyName )) { // issue #448 ask first for additional params
                        value = this.newBoundSql.getAdditionalParameter( propertyName );
                    } else if (this.parameterObject == null) {
                        value = null;
                    } else if (this.typeHandlerRegistry.hasTypeHandler( this.parameterObject.getClass() )) {
                        value = parameterObject;
                    } else {
                        MetaObject metaObject = this.configuration.newMetaObject( this.parameterObject );
                        value = metaObject.getValue( propertyName );
                    }
                    System.out.println( "++++++++++++++++++++++value++++++++++++++++++++++" + value );
                    TypeHandler typeHandler = parameterMapping.getTypeHandler();
                    JdbcType jdbcType = parameterMapping.getJdbcType();
                    if (value == null && jdbcType == null) {
                        jdbcType = this.configuration.getJdbcTypeForNull();
                    }
                    try {
                        typeHandler.setParameter( ps, i + 1, value, jdbcType );
                    } catch (TypeException | SQLException e) {
                        throw new TypeException( "Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e );
                    }
                }
            }
        }
    }
}
